from passlib.context import CryptContext
import motor.motor_asyncio
from jose import JWTError, jwt
from typing import Optional, List
from datetime import datetime, timedelta
from schema.dao.user import UserInDb
from config import oauth_conf
from schema.resp.user import UserInList
from schema.req.user import UserUpdateInfo
from schema.dao.user import Role
from pymongo.results import UpdateResult


AsyncIOMotorCollection = motor.motor_asyncio.AsyncIOMotorCollection


pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')  # used for hash and verify pwd


def _verify_pwd(plain_pwd, hashed_pwd) -> bool:
    return pwd_context.verify(plain_pwd, hashed_pwd)


def _get_pwd_hash(pwd: str) -> str:
    return pwd_context.hash(pwd)


async def find_user_by_username(coll: AsyncIOMotorCollection, username: str) -> Optional[UserInDb]:
    user_doc = await coll.find_one({'username': username})
    if not user_doc:
        return None
    user_in_db = UserInDb(
        username=user_doc['username'],
        screen_name=user_doc['screen_name'],
        role=user_doc['role'],
        token=user_doc['token'],
        hashed_pwd=user_doc['hashed_pwd'],
        description=user_doc.get('description')
    )
    return user_in_db


async def authenticate_user(coll: AsyncIOMotorCollection, username: str, password: str) -> Optional[UserInDb]:
    """
    验证一个 username 与 pwd 是否合法
    :param coll: 存放用户数据的 mongo 集合
    :param username: 用户名
    :param password: 密码
    :return: 对应的用户信息，若没有该用户则返回 None
    """
    user_in_db = await find_user_by_username(coll, username)
    if not user_in_db:
        return None
    if not _verify_pwd(password, user_in_db.hashed_pwd):
        return None
    return user_in_db


def create_access_token(payload: dict) -> str:
    """
    创建一个 JWT token
    :param payload: 存放于 token 中的数据
    :return: 生成的 token
    """
    encoded_jwt = jwt.encode(payload, oauth_conf.SECRET_KEY, algorithm=oauth_conf.ALGORITHM)
    return encoded_jwt


async def create_new_user(coll: AsyncIOMotorCollection, username: str, password: str, screen_name: str, role: str, description: str) -> Optional[UserInDb]:
    """
    根据用户信息创建一个新的用户
    :return: 创建成功则返回新的 user，若数据库中已存在则返回 None
    """
    user_in_db = await find_user_by_username(coll, username)
    if user_in_db:
        return None  # 用户已存在，返回 None
    hashed_pwd = _get_pwd_hash(password)
    new_user = UserInDb(username=username, screen_name=screen_name, role=role, description=description, token=None, hashed_pwd=hashed_pwd)
    return new_user


async def update_user_token(coll: AsyncIOMotorCollection, username: str, token: Optional[str]) -> UpdateResult:
    return await coll.update_one({'username': username}, {'$set': {'token': token}})


async def delete_one_user(coll: AsyncIOMotorCollection, username: str):
    result = await coll.delete_one({'username': username})
    return result


async def get_all_user(coll: AsyncIOMotorCollection) -> List[UserInList]:
    return [
        UserInList(username=user['username'], screen_name=user['screen_name'], role=user['role'], description=user.get('description'))
        async for user in coll.find()
    ]


async def update_user_pwd(coll: AsyncIOMotorCollection, username: str, new__raw_pwd: str) -> UpdateResult:
    hashed_pwd = _get_pwd_hash(new__raw_pwd)
    return await coll.update_one({'username': username}, {'$set': {'hashed_pwd': hashed_pwd}})


async def update_user_screen_name(coll: AsyncIOMotorCollection, username: str, screen_name: str) -> UpdateResult:
    return await coll.update_one({'username': username}, {'$set': {'screen_name': screen_name}})


async def update_user_role(coll: AsyncIOMotorCollection, username: str, role: Role) -> UpdateResult:
    return await coll.update_one({'username': username}, {'$set': {'role': role.value}})


async def update_user_info(coll: AsyncIOMotorCollection, update_info: UserUpdateInfo):
    if update_info.password:
        update_info.password = _get_pwd_hash(update_info.password)
    mongo_update_fields = {k: v for k, v in update_info.dict().items() if v is not None}
    mongo_update_fields.pop('username')
    mongo_update_fields.pop('password')
    mongo_update_fields['hashed_pwd'] = update_info.password
    return await coll.update_one({'username': update_info.username}, {'$set': mongo_update_fields})

